﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Reflection.Emit;
using System.Collections.ObjectModel;

namespace Ganaye.EmbeddedDb
{

    class ProxyFactory
    {
        private static ProxyFactory mInstance;

        public static ProxyFactory Instance
        {
            get
            {
                if (mInstance == null) mInstance = new ProxyFactory();
                return mInstance;
            }
        }

        private ModuleBuilder mModuleBuilder;

        private ProxyFactory()
        {
            AppDomain ad = AppDomain.CurrentDomain;

            //var ad = domain;

            // create a new dynamic assembly
            AssemblyName an = new AssemblyName("EmbeddedDbDynamicProxy");
            AssemblyBuilder ab = ad.DefineDynamicAssembly(
             an, AssemblyBuilderAccess.Run);

            // create a new module to hold code in the assembly
            mModuleBuilder = ab.DefineDynamicModule(an.Name);

        }

        public Type GenerateProxy(
            Type baseType,
            string typeName,
            List<ColumnDefinition> columns)
        {
            if (baseType.IsPublic == false)
                throw new RuntimeException(string.Format("Can't generate proxy. Type {0} must be public (and not within a private class).", baseType.Name));
            // create a baseType in the module
            TypeBuilder typeBuilder = mModuleBuilder.DefineType(
             typeName, TypeAttributes.Public, baseType);

            ILGenerator gen;
            // generate the MSIL
            int columnNo = 0;
            int bufferSize = 1;
            foreach (var entry in columns)
            {
                ColumnDefinition column = entry;
                string propertyName = column.ColumnName;
                
                PropertyInfo pi = column.BaseProperty;

                bool ownField = (pi == null || pi.GetGetMethod().IsAbstract);
                FieldBuilder lNewField;
                if (ownField)
                {
                    if (column.FieldType == null) column.GuessFieldType();
                    lNewField = typeBuilder.DefineField(
                        "m" + column.ColumnName,
                        column.FieldType,
                        FieldAttributes.Private
                    );
                }
                else lNewField = null;

                bool isOverride;
                MethodInfo getMethod;
                MethodInfo setMethod;
                
                
                if (pi == null)
                {
                    isOverride = false;
                    pi = typeBuilder.DefineProperty(column.ColumnName,
                        PropertyAttributes.None,
                        column.FieldType,
                        Type.EmptyTypes);
                    getMethod = typeBuilder.DefineMethod("get_" + column.ColumnName,
                        MethodAttributes.Public
                        | MethodAttributes.SpecialName
                        | MethodAttributes.HideBySig,
                        column.FieldType, Type.EmptyTypes);
                    setMethod = typeBuilder.DefineMethod("set_" + column.ColumnName,
                        MethodAttributes.Public
                        | MethodAttributes.SpecialName
                        | MethodAttributes.HideBySig,
                        typeof(void), new Type[] { column.FieldType });
                }
                else
                {
                    isOverride = true;
                    getMethod = pi.GetGetMethod(true);
                    setMethod = pi.GetSetMethod(true);
                }
                var newGet = EmitMethod(typeBuilder, pi, getMethod, lNewField, columnNo, 0, isOverride);
                var newSet = EmitMethod(typeBuilder, pi, setMethod, lNewField, columnNo, 0, isOverride);

                
                var writeMethod = typeBuilder.DefineMethod("write_" + column.ColumnName,
                    MethodAttributes.Public
                    | MethodAttributes.SpecialName
                    | MethodAttributes.HideBySig
                    | MethodAttributes.Static,
                    typeof(void),
                    new Type[] { typeof(Record) });

                gen = writeMethod.GetILGenerator();

                gen.Emit(OpCodes.Ldarg_0);                   // L_0001: ldarg.0 
                // this
                gen.Emit(OpCodes.Ldc_I4, columnNo);          // L_000c: ldc.i4.2 
                // this, columnNo
                gen.Emit(OpCodes.Call, mRecord_GetColumn3);
                // column
                gen.Emit(OpCodes.Ldarg_0);                   //L_0012: ldarg.0 
                // column, record
                gen.Emit(OpCodes.Ldarg_0);                   //L_0013: ldarg.0 
                // column, record, record
                gen.Emit(OpCodes.Ldfld, lNewField);                 //L_0014: ldflda valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.DateTime> Ganaye.EmbeddedDb.ClientProxy::mDob
                // column, record, record.mDob

                MethodInfo mi_WriteValueInBuffer = typeof(TableColumn<>).MakeGenericType(new Type[] { lNewField.FieldType })
                    .GetMethod(
                    "WriteValueInBuffer",
                    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
                gen.Emit(OpCodes.Callvirt, mi_WriteValueInBuffer);

                gen.Emit(OpCodes.Ret);


                if (isOverride)
                {
                    typeBuilder.DefineMethodOverride(newGet, getMethod);
                    typeBuilder.DefineMethodOverride(newSet, setMethod);
                }
                else
                {
                    ((PropertyBuilder)pi).SetGetMethod(newGet);
                    ((PropertyBuilder)pi).SetSetMethod(newSet);
                }
                columnNo++;

                bufferSize += entry.Size;
            }
            

            var readBufferField = typeBuilder.DefineField("mReadBuffer", typeof(Byte[]), FieldAttributes.Private);

            var oldGetReadBuffer = typeof(Record).GetMethod("GetReadBuffer");
            var newGetReadBuffer = typeBuilder.DefineMethod("GetReadBuffer", MethodAttributes.Public | MethodAttributes.Virtual, CallingConventions.Standard, typeof(Byte[]), System.Type.EmptyTypes);
            gen = newGetReadBuffer.GetILGenerator();
            
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldfld, readBufferField);
            gen.Emit(OpCodes.Ret);
            typeBuilder.DefineMethodOverride(newGetReadBuffer, oldGetReadBuffer);

            var constr = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes);
            gen = constr.GetILGenerator();
            var baseConstrType = typeBuilder.BaseType;
            while (baseConstrType != null)
            {
                var baseConstr = baseConstrType.GetConstructor(Type.EmptyTypes);
                if (baseConstr != null)
                {
                    gen.Emit(OpCodes.Ldarg_0);
                    gen.Emit(OpCodes.Call, baseConstr);
                    break;
                }
                baseConstrType = baseConstrType.BaseType;
            }
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldc_I4, columnNo);
            gen.Emit(OpCodes.Newarr, typeof(FieldState));
            gen.Emit(OpCodes.Stfld, mFi_Record_mFieldStates);
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldc_I4, bufferSize);
            gen.Emit(OpCodes.Newarr, typeof(Byte));
            gen.Emit(OpCodes.Stfld, readBufferField);
            gen.Emit(OpCodes.Ret);

            var methCreateRecord = typeBuilder.DefineMethod("CreateRecord",
                MethodAttributes.Static | MethodAttributes.Public,
                CallingConventions.Standard,
                typeof(Record),
                Type.EmptyTypes);
            gen = methCreateRecord.GetILGenerator();
            gen.Emit(OpCodes.Newobj, constr);
            gen.Emit(OpCodes.Ret);

            

            Type t = typeBuilder.CreateType();

            return t;
        }

        static FieldInfo mFi_Record_mTable = typeof(Record).GetField("mTable", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
        static FieldInfo mFi_Record_mFieldStates = typeof(Record).GetField("mFieldStates", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
        static MethodInfo mRecord_GetColumn1 = typeof(Record).GetMethod("GetColumn1", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
        static MethodInfo mRecord_GetColumn2 = typeof(Record).GetMethod("GetColumn2", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
        static MethodInfo mRecord_GetColumn3 = typeof(Record).GetMethod("GetColumn3", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
        static MethodInfo mMi_List_of_TableColumn_get_Item = typeof(ReadOnlyCollection<TableColumn>).GetProperty("Item").GetGetMethod(true);
        static MethodInfo mMi_Program_Debug = typeof(Program).GetMethod("Debug", new Type[] { typeof(object) });
        static MethodInfo mMi_Console_WriteLine = typeof(Console).GetMethod("WriteLine", new Type[] { typeof(object) });
        private MethodBuilder EmitMethod(
            TypeBuilder tb,
            System.Reflection.PropertyInfo pi,
            MethodInfo mi,
            FieldBuilder newField,
            int columnNo,
            int offset,
            bool isOverride)
        {

            // we have to implement something
            Type returntype = mi.ReturnType;
            Type[] paramstype = new Type[] { };
            MethodAttributes attrs = mi.Attributes;
            MethodBuilder methb;

            if (isOverride)
            {
                ParameterInfo[] paramInfos = mi.GetParameters();
                paramstype = new Type[paramInfos.Length];
                for (int i = 0; i < paramstype.Length; i++)
                {
                    paramstype[i] = paramInfos[i].ParameterType;
                }
                attrs |= MethodAttributes.Virtual;
                attrs &= ~MethodAttributes.Abstract;
                methb = tb.DefineMethod(mi.Name,
                    attrs, returntype, paramstype);
            }
            else
            {
                methb = (MethodBuilder)mi;
            }

            ILGenerator gen = methb.GetILGenerator();

            if (returntype == typeof(void))
            {

                // Emit the SET_Field method
                //    ((TableColumnNullableDateTime)mTable.Columns[2]).ChangeCurrentValue(this, ref mDob, value);

                gen.Emit(OpCodes.Ldarg_0);                   // L_0001: ldarg.0 
                // this
                gen.Emit(OpCodes.Ldc_I4, columnNo);          // L_000c: ldc.i4.2 
                // this, columnNo
                gen.Emit(OpCodes.Call, mRecord_GetColumn2);
                // tableColumn
                Type tableColumnType = typeof(TableColumn<>).MakeGenericType(new Type[] { newField.FieldType });
                //gen.Emit(OpCodes.Castclass, tableColumnType); <- not nescessary apparently.
                // tableColumn<Int>
                gen.Emit(OpCodes.Ldarg_0);                   //L_0012: ldarg.0 
                // tableColumn<Int>, this
                gen.Emit(OpCodes.Ldarg_0);                   //L_0013: ldarg.0 
                // tableColumn<Int>, this, this
                gen.Emit(OpCodes.Ldflda, newField);                 //L_0014: ldflda valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.DateTime> Ganaye.EmbeddedDb.ClientProxy::mDob
                // tableColumn<Int>, this, &this.mDob
                gen.Emit(OpCodes.Ldarg_1);                   // value
                // column, this, &this.mDob, value

                //L_0012: castclass Ganaye.EmbeddedDb.TableColumnInt32

                MethodInfo mi_ChangeCurrentValue = tableColumnType.GetMethod(
                    "ChangeCurrentValue",
                    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

                gen.Emit(OpCodes.Callvirt, mi_ChangeCurrentValue);
                //gen.Emit(OpCodes.Pop);
                //gen.Emit(OpCodes.Pop);
                //gen.Emit(OpCodes.Pop);
                //gen.Emit(OpCodes.Pop);
                /*

                // L_0019: call instance void Ganaye.EmbeddedDb.TableColumn::GetValue(class Ganaye.EmbeddedDb.Record, valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.DateTime>&)
              */
            }
            else
            {
                // Emit the GET_Field method

                gen.Emit(OpCodes.Ldarg_0);                   // L_0001: ldarg.0 
                // this
                gen.Emit(OpCodes.Ldc_I4, columnNo);          // L_000c: ldc.i4.2 
                // this, columnNo
                gen.Emit(OpCodes.Call, mRecord_GetColumn1);
                // column
                gen.Emit(OpCodes.Ldarg_0);                   //L_0012: ldarg.0 
                // column, this
                gen.Emit(OpCodes.Ldarg_0);                   //L_0013: ldarg.0 
                // column, this, this
                gen.Emit(OpCodes.Ldflda, newField);                 //L_0014: ldflda valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.DateTime> Ganaye.EmbeddedDb.ClientProxy::mDob
                // column, this, &this.mDob

                MethodInfo mi_ReadBufferValue = typeof(TableColumn<>).MakeGenericType(new Type[] { newField.FieldType })
                    .GetMethod(
                    "ReadBufferValue",
                    BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
                gen.Emit(OpCodes.Callvirt, mi_ReadBufferValue);
                // L_0019: call instance void Ganaye.EmbeddedDb.TableColumn::GetValue(class Ganaye.EmbeddedDb.Record, valuetype [mscorlib]System.Nullable`1<valuetype [mscorlib]System.DateTime>&)
                // <empty>
                gen.Emit(OpCodes.Ldarg_0);
                gen.Emit(OpCodes.Ldfld, newField);
            }
            gen.Emit(OpCodes.Ret);
            return methb;
        }

    }
}
